요약
이 기사에서는 Unreal Insights를 사용하여 최적화 부하 조사를 수행하는 과정과, 게임 내에서 자산을 LoadSynchronous()
로 동기적으로 로드할 때 발생하는 히치를 방지하기 위한 대책으로 C++로 비동기 로드를 구현하는 방법을 소개합니다. 블루프린트에서의 비동기 로드에 대한 정보는 아래의 참조 링크를 참조하십시오.
환경
- Rider 2024.2.6
- Unreal Engine 5.4
- Windows 11 Pro
참고
- 공식 문서 Asynchronous Asset Loading
- [UE4] Asset Manager의 비동기 로드 기능에 대하여 1부 (비동기 로드 설명 및 레벨 배경 로드 편)
- Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
- 특히 22:40에서 28:15까지 비동기 로드에 대한 정보가 제공되며, 27:33에서 블루프린트를 사용한 비동기 로드(AsyncLoad)의 구현 방법이 소개됩니다.
- [UE5] Unreal Insights 사용해 보기
- Unreal Insights
본문
Unreal Insights를 이용한 최적화 부하 조사
Unreal Insights란?
Unreal Insights는 Unreal Engine에서 성능 및 메모리 사용량을 분석하기 위한 도구입니다. 공식 문서는 여기를 참조하십시오: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine
사용 방법
사용 방법에 대한 자세한 내용은 [UE5] Unreal Insights 사용해 보기를 참조하십시오.
최적화 조사를 수행할 때는 일반적인 PIE(Play In Editor) 모드가 아닌 타겟 플랫폼의 패키지 버전을 사용하는 것이 이상적입니다. PIE 모드에서는 미리 로드된 캐시와 백그라운드 작업이 영향을 미쳐 정확한 부하 측정이 어려워지기 때문입니다.
이번에는 PIE에 가까운 환경인 Standalone Game에서 조사를 수행할 것입니다.
Standalone Game 시작 방법
UE 에디터에서 아래 그림처럼 Standalone Game을 시작합니다.
게임 플레이 중에 히치(멈춤)가 발생할 경우, 다음 명령어를 사용해 보세요.
stat unitgraph
"stat UnitGraph" 명령어는 게임의 처리 부하를 그래프 형식으로 시각화하는 도구입니다. 이를 통해 히치가 발생하는 시점에 그래프에 큰 스파이크가 표시되어 문제의 위치를 명확하게 식별할 수 있습니다.
많은 사람들이 자주 사용하는 "stat Unit"이나 "stat fps"와는 달리, "stat UnitGraph"는 짧은 시간의 히치도 그래프에 남기므로 놓치는 경우가 적습니다.
실행 후에는 아래와 같은 그래프가 표시됩니다. 왼쪽 아래에는 부하 그래프가, 오른쪽 위에는 구체적인 수치가 표시됩니다. 60fps를 목표로 할 경우, Frame 시간을 16.6ms 이하로 유지하는 것이 이상적입니다.
다음으로 Unreal Insights를 사용하여 실제로 측정을 진행합니다.
trace.start
및 trace.stop
Unreal Insights로 측정을 시작하려면 trace.start
명령어를 실행합니다. 측정이 완료되면 trace.stop
으로 종료합니다.
처리 부하 측정 결과
아래 비디오와 같이 히치 발생이 확인되었습니다.
무거운 처리가 실행되면 그래프에 큰 변화가 나타납니다.
게임: 58.26ms
게임 스레드가 58.26ms의 시간을 소요하고 있음을 알 수 있습니다.
다음으로 Unreal Insights의 측정 결과도 확인해 보겠습니다.
Trace 열기 방법
Trace 데이터를 열면 다음과 같은 결과가 표시됩니다.
이 결과에서 주목할 점은 초록색 바로 표시된
LoadObject (154.7ms) - /Game/Main/InGame/VFX/Niagara/NS_DizzyStar.NS_DizzyStar
입니다. 여기서 나이아가라 효과의 로딩 프로세스가 큰 부하를 주고 있다는 것을 알 수 있습니다.
특히 Niagara의 초기 로드나 스폰 시 셰이더 컴파일로 인해 히치가 발생할 수 있습니다.
이번에는 초기 로드 시 히치가 발생하는 원인에 대해 설명하겠습니다. 하지만, 초기 스폰 시 히치가 발생하는 경우, 사전(화면이 어두워진 동안 등)에 한 번 보이지 않는 위치에서 스폰시키는 방법으로 해결할 수 있습니다.
패키지 버전에서는 설치 후 처음 1회만 이 히치가 발생합니다.
UE에서는 UE 시작 후 처음 1회만 발생합니다.
히치를 재현하고 싶다면, 언리얼 엔진을 재시작하거나 패키지를 삭제하고 재설치해야 합니다.
이것이 원인인 나이아가라 효과입니다.
C++ 코드를 보면 로딩 프로세스는 다음과 같습니다.
PlayerCharacter.h1public: 2 //... 3 UPROPERTY(EditAnywhere, BlueprintReadWrite) 4 TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset; 5 //...
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.LoadSynchronous(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
이펙트를 스폰하기 직전에 LoadSynchronous()
로 이펙트 자산을 동기적으로 로드하는 것이 히치를 발생시키고 있습니다.
LoadSynchronous()
(동기 로드)는 로드가 완료될 때까지 기다린다는 뜻입니다(다른 처리를 중단). 이로 인해 플레이어는 히치를 느끼게 됩니다.
UE 공식에서는 비동기 로드를 권장합니다.
참고 영상: Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
영상의 22:40부터 28:15까지, 27:33에서는 Blueprint에서 비동기 로드(AsyncLoad)의 구현 방법이 소개됩니다.
TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset
이펙트 자산은 플레이어가 소지하고 있습니다.
조사 결과: 원인은 이펙트를 스폰하기 직전의 LoadSynchronous()
입니다.
비동기 로드(AsyncLoad) 구현하기
동기 로드로 인한 히치를 없애기 위해, 게임 시작 시 자산을 사전에 비동기적으로 로드합니다.
비동기 로드에는 시간이 걸릴 수 있으므로, 사전 로드를 하지 않으면 자산의 스폰이 적시에 이루어지지 않을 수 있습니다.
다음으로, C++에서 비동기 로드를 위한 함수를 생성하겠습니다.
PlayerCharacter.h1protected: 2 void OnDizzyEffectLoaded(); 3 void LoadDizzyEffectAsset();
PlayerCharacter.cpp1void APlayerCharacter::BeginPlay() 2{ 3 Super::BeginPlay(); 4 LoadDizzyEffectAsset(); 5} 6 7void APlayerCharacter::LoadDizzyEffectAsset() 8{ 9 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset requeset load")); 10 UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(DizzyEffectAsset.ToSoftObjectPath(), 11 FStreamableDelegate::CreateUObject( 12 this, &APlayerCharacter::OnDizzyEffectLoaded)); 13} 14 15void APlayerCharacter::OnDizzyEffectLoaded() 16{ 17 UE_LOG(LogTemp, Log, TEXT("DizzyEffectLoaded")); 18 19 if (IsValid(DizzyEffectAsset.Get())) 20 { 21 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset is valid")); 22 } 23 else 24 { 25 UE_LOG(LogTemp, Error, TEXT("DizzyEffectAsset is null, Function name: %s"), *FString(__FUNCTION__)); 26 } 27}
다음으로, 자산의 동기 로드를 Get()
으로 대체하세요.
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.Get(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
이로써 비동기 로드 구현이 완료되었습니다.
다음으로 Standalone 게임에서 확인해 보겠습니다.
로그에 DizzyEffectAsset is valid
가 표시되면, 자산의 비동기 로드(AsyncLoad)가 성공했음을 확인할 수 있습니다.
결과
「stat UnitGraph」 명령을 사용한 결과, 그래프의 스파이크가 사라졌고, 히치가 해소되었습니다.
요약
조사 흐름
- 히치 탐지: 게임 중 발생한 히치를 「stat unitgraph」 명령을 사용하여 그래프로 나타내고, 무거운 처리가 발생하는 위치를 확인했습니다.
- Unreal Insight로 측정: 히치 발생 시의 상세한 부하를 조사하기 위해 trace.start 및 trace.stop 명령을 사용하여 추적 데이터를 수집하고 부하 발생 원인을 확인했습니다.
문제 확인
- 이펙트 자산(Niagara 이펙트)이 동기 로드
LoadSynchronous()
에 의해 로드되고 있었기 때문에, 로드가 완료될 때까지 다른 처리가 중지되어 히치가 발생한 것으로 확인되었습니다.
비동기 로드 구현
- 히치를 피하기 위해 TSoftObjectPtr를 사용하여 게임 시작 시 이펙트를 비동기적으로 로드하는 과정을 추가했습니다. 이를 통해 플레이어가 이펙트를 사용하기 전에 자산이 로드되어 히치를 방지할 수 있습니다.
결과
비동기 로드 구현으로 히치가 해소되어 게임 플레이가 매끄러워졌습니다. 비동기 로드를 활용하여 게임 성능을 향상시키고 플레이어 경험을 개선할 수 있었습니다.